Make the container deployable on Dokploy
domains.json was written next to server.js, i.e. into the image filesystem, so every redeploy discarded the domain -> glossary_id mappings. Its path is now overridable via DOMAINS_FILE and the image defaults it to /app/data, a subdirectory so that mounting a volume there does not shadow server.js and index.html in /app. Also adds a GET /health endpoint plus a Docker HEALTHCHECK that uses it. The probe deliberately does not call DeepL, so an upstream DeepL outage does not get the container restarted. .gitignore is new; .env holds a real API key and was previously untracked only by convention. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
.env
|
.env
|
||||||
.env.example
|
.env.example
|
||||||
|
.env.testcopy
|
||||||
domains.json
|
domains.json
|
||||||
|
data
|
||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
*.md
|
*.md
|
||||||
|
|||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
.env.testcopy
|
||||||
|
node_modules
|
||||||
|
data/
|
||||||
94
DEPLOY.md
Normal file
94
DEPLOY.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Deploying to Dokploy
|
||||||
|
|
||||||
|
The app is a single Node process with no dependencies. Dokploy builds it from
|
||||||
|
the `Dockerfile` in this repo and puts Traefik in front of it.
|
||||||
|
|
||||||
|
## 1. Push the repo to a Git host
|
||||||
|
|
||||||
|
Dokploy pulls from GitHub/GitLab/Gitea (or a raw Git URL over SSH).
|
||||||
|
|
||||||
|
**Before the first push:** `.env` and `.env.testcopy` contain a real DeepL API
|
||||||
|
key. They are untracked from now on via `.gitignore`, but if they were already
|
||||||
|
committed they still exist in history — remove them from tracking and rotate
|
||||||
|
the key rather than relying on the file being gone from the working tree.
|
||||||
|
|
||||||
|
## 2. Create the application in Dokploy
|
||||||
|
|
||||||
|
Project → **Create Service** → **Application**.
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------- | ------------------------------ |
|
||||||
|
| Source | Git provider + this repo/branch |
|
||||||
|
| Build Type | **Dockerfile** |
|
||||||
|
| Dockerfile | `Dockerfile` |
|
||||||
|
|
||||||
|
## 3. Environment variables
|
||||||
|
|
||||||
|
**Environment** tab:
|
||||||
|
|
||||||
|
```
|
||||||
|
DEEPL_API_KEY=<your DeepL Growth API key>
|
||||||
|
```
|
||||||
|
|
||||||
|
`PORT` and `DOMAINS_FILE` are already set in the Dockerfile — do not override
|
||||||
|
them unless you also change the volume mount path to match.
|
||||||
|
|
||||||
|
The key is server-side only; it is never sent to the browser.
|
||||||
|
|
||||||
|
## 4. Volume (required)
|
||||||
|
|
||||||
|
Without this, every redeploy resets all glossary mappings.
|
||||||
|
|
||||||
|
**Advanced → Volumes → Add** :
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ----------- | -------------- |
|
||||||
|
| Type | Volume Mount |
|
||||||
|
| Volume Name | `deepl-data` |
|
||||||
|
| Mount Path | `/app/data` |
|
||||||
|
|
||||||
|
`DOMAINS_FILE=/app/data/domains.json` writes into that volume. The path is a
|
||||||
|
subdirectory of `/app` on purpose — mounting `/app` itself would hide
|
||||||
|
`server.js` and `index.html`.
|
||||||
|
|
||||||
|
## 5. Domain
|
||||||
|
|
||||||
|
**Domains** tab → Add:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------- | ------------------------ |
|
||||||
|
| Host | your hostname |
|
||||||
|
| Container Port| `3000` |
|
||||||
|
| HTTPS | on, Let's Encrypt |
|
||||||
|
|
||||||
|
## 6. Deploy
|
||||||
|
|
||||||
|
Hit **Deploy**. Verify with:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://<your-host>/health
|
||||||
|
# {"status":"ok","apiKeyConfigured":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
`apiKeyConfigured: false` means `DEEPL_API_KEY` did not reach the container.
|
||||||
|
|
||||||
|
## Seeding existing glossary mappings
|
||||||
|
|
||||||
|
A fresh volume starts empty and the app writes a default `domains.json` with
|
||||||
|
all four domains and no glossaries. The `domains.json` in this repo is **not**
|
||||||
|
copied into the image (it is in `.dockerignore`), so any glossary IDs recorded
|
||||||
|
there are not carried over. Either re-save the glossaries through the UI after
|
||||||
|
the first deploy, or copy the file in once:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker cp domains.json <container>:/app/data/domains.json
|
||||||
|
docker restart <container>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The app has **no authentication**. Anything on the public domain can use your
|
||||||
|
DeepL quota and edit the shared glossaries. Put Traefik basic auth or an
|
||||||
|
IP allowlist in front of it if it is internet-facing.
|
||||||
|
- Docker `HEALTHCHECK` hits `/health` every 30s. It does not call DeepL, so a
|
||||||
|
DeepL outage will not restart the container.
|
||||||
17
Dockerfile
17
Dockerfile
@@ -7,10 +7,21 @@ FROM node:18-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY server.js index.html package.json ./
|
COPY server.js index.html package.json ./
|
||||||
|
|
||||||
# DEEPL_API_KEY is passed at "docker run" time (via -e or --env-file), not
|
# DEEPL_API_KEY is passed at runtime (docker run -e / --env-file, or the
|
||||||
# baked into the image. domains.json should be bind-mounted so glossary
|
# Dokploy "Environment" tab), never baked into the image.
|
||||||
# data survives container restarts/recreation (see README).
|
#
|
||||||
|
# domains.json holds which DeepL glossary_id belongs to which domain and
|
||||||
|
# direction. It must live on a mounted volume, otherwise every redeploy
|
||||||
|
# builds a fresh filesystem and the mappings are lost. /app/data is a
|
||||||
|
# subdirectory so mounting it does not shadow the app files in /app.
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
ENV DOMAINS_FILE=/app/data/domains.json
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
14
server.js
14
server.js
@@ -66,7 +66,10 @@ function baseUrlFor(apiKey) {
|
|||||||
// the server disk, not in any user's browser, so every visitor to the site
|
// the server disk, not in any user's browser, so every visitor to the site
|
||||||
// sees and edits the same shared glossaries.
|
// sees and edits the same shared glossaries.
|
||||||
|
|
||||||
const DOMAINS_FILE = path.join(__dirname, 'domains.json');
|
// The path is overridable via DOMAINS_FILE so a container deployment can point
|
||||||
|
// it at a mounted volume (e.g. /app/data/domains.json) instead of the image's
|
||||||
|
// own filesystem, which is recreated on every redeploy.
|
||||||
|
const DOMAINS_FILE = process.env.DOMAINS_FILE || path.join(__dirname, 'domains.json');
|
||||||
const VALID_DOMAINS = ['scheer-group', 'scheer-ids', 'scheer-pas', 'scheer-imc'];
|
const VALID_DOMAINS = ['scheer-group', 'scheer-ids', 'scheer-pas', 'scheer-imc'];
|
||||||
const DOMAIN_LABELS = {
|
const DOMAIN_LABELS = {
|
||||||
'scheer-group': 'Scheer Group',
|
'scheer-group': 'Scheer Group',
|
||||||
@@ -137,6 +140,7 @@ function loadDomains() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveDomains(data) {
|
function saveDomains(data) {
|
||||||
|
fs.mkdirSync(path.dirname(DOMAINS_FILE), { recursive: true });
|
||||||
fs.writeFileSync(DOMAINS_FILE, JSON.stringify(data, null, 2));
|
fs.writeFileSync(DOMAINS_FILE, JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +452,15 @@ function serveStatic(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liveness probe for container platforms (Docker HEALTHCHECK, Dokploy).
|
||||||
|
// Deliberately does not call DeepL: it answers "is this process serving?",
|
||||||
|
// not "is the upstream API up", so a DeepL outage doesn't restart the app.
|
||||||
|
function handleHealth(req, res) {
|
||||||
|
sendJson(res, 200, { status: 'ok', apiKeyConfigured: Boolean(DEEPL_API_KEY) });
|
||||||
|
}
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
|
'GET /health': handleHealth,
|
||||||
'POST /api/translate': handleTranslate,
|
'POST /api/translate': handleTranslate,
|
||||||
'POST /api/usage': handleUsage,
|
'POST /api/usage': handleUsage,
|
||||||
'POST /api/document/upload': handleDocumentUpload,
|
'POST /api/document/upload': handleDocumentUpload,
|
||||||
|
|||||||
Reference in New Issue
Block a user