| cmd | ||
| deploy | ||
| docs | ||
| example | ||
| internal | ||
| migrations | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| DEVELOPMENT_WORKFLOW.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| IMPLEMENTATION_PLAN.md | ||
| Justfile | ||
| migrate | ||
| PRODUCT_SPEC.md | ||
| README.md | ||
| server | ||
Update Server
Self-hosted server for publishing application updates. An administrator uploads versioned release artifacts through a web UI, and client applications discover and download allowed updates through a bearer-authenticated HTTP API.
The project is designed for small private deployments: one Go application, one SQLite database, local artifact storage, and HTTPS termination by a reverse proxy such as Caddy or Nginx.
What It Does
- Admin web UI at
/admin. - Project and tag management.
- Release artifact upload per project.
- Client API key management with project or tag scope rules.
- Client API at
/api/v1. - Private artifact downloads through the application, not through a public file directory.
- SQLite migrations applied automatically on startup.
- Docker image and Docker Compose deployment files.
Current Readiness
This is a usable MVP. It is ready to run locally or on a small server with Docker, create an admin user, create projects, upload releases, issue API keys, and let clients check and download updates.
You do not need to prepare a separate database or run a separate migration step.
The container stores SQLite and uploaded artifacts under /data, and the app
applies migrations on startup.
You do need:
- Docker with Docker Compose;
APP_BASE_URL,ADMIN_EMAIL, andADMIN_PASSWORD;- a persistent
/datavolume; - for public HTTPS: a reverse proxy in front of the app.
Post-MVP features such as release channels, signed manifests, delta updates, S3-compatible storage, and multi-user administration are not part of the current minimal version.
Quick Start With Docker Compose
-
Copy the example environment file:
cp deploy/update-server.env.example .env -
Edit
.env.For a local first run:
APP_BASE_URL=http://127.0.0.1:8080 ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=change-this-before-productionFor production behind HTTPS:
APP_BASE_URL=https://updates.example.com ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=use-a-real-passwordIf
APP_BASE_URLstarts withhttps://, open the admin UI through that HTTPS URL. Secure cookies are enabled automatically for HTTPS base URLs. -
Build and start the container:
docker compose up -d --build -
Check that it is alive:
curl http://127.0.0.1:8080/healthz -
Open the admin UI:
http://127.0.0.1:8080/admin
After login, create a project, upload a release, create an API key with download permission, and use that key from a client.
Docker Run Example
Docker Compose is the recommended path, but a direct Docker run works too:
docker build -t update-server:latest .
docker volume create update-server-data
docker run -d \
--name update-server \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e APP_ADDR=0.0.0.0:8080 \
-e APP_BASE_URL=http://127.0.0.1:8080 \
-e DATA_DIR=/data \
-e SQLITE_PATH=/data/db.sqlite \
-e ARTIFACTS_DIR=/data/artifacts \
-e MIGRATIONS_DIR=/app/migrations \
-e TEMPLATES_DIR=/app/web/templates \
-e STATIC_DIR=/app/web/static \
-e TRUST_PROXY_HEADERS=false \
-e ADMIN_EMAIL=admin@example.com \
-e ADMIN_PASSWORD=change-this-before-production \
-v update-server-data:/data \
update-server:latest
Proxmox / Caddy Example
A simple home-server layout is:
Internet
-> router port forward 80/443
-> Caddy reverse proxy LXC
-> Docker tools LXC running update-server
Example:
Caddy LXC: 192.168.1.50
Docker tools LXC: 192.168.1.130
Update Server: 192.168.1.130:18080
Public URL: https://updates.esenin.site
In this layout, publish the container port on the Docker tools LXC address:
ports:
- "192.168.1.130:18080:8080"
Set production environment values:
APP_BASE_URL=https://updates.esenin.site
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=use-a-real-password
Then point Caddy at the internal service:
updates.esenin.site {
encode gzip zstd
request_body {
max_size 1GB
}
reverse_proxy 192.168.1.130:18080
}
The admin UI and API can live on the same domain:
https://updates.esenin.site/admin
https://updates.esenin.site/api/v1
Client API
The API entrypoint is:
GET /api/v1
Protected client endpoints require:
Authorization: Bearer <api_key>
Common endpoints:
GET /api/v1/projects
GET /api/v1/projects/{projectSlug}/releases/latest
GET /api/v1/releases/{releaseID}
GET /api/v1/releases/{releaseID}/download
Example:
curl \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/projects
Downloads are intentionally private: uploaded files should not be served by Caddy or Nginx as static files. Clients download through the application so API key permissions and project scopes are enforced.
Data And Backups
Persist the whole /data directory or Docker volume. It contains:
/data/db.sqlite
/data/db.sqlite-wal
/data/db.sqlite-shm
/data/artifacts/...
For live backups, prefer SQLite's online backup command for the database and
archive /data/artifacts separately. See docs/DEPLOYMENT.md for a fuller
backup and restore flow.
Development
Useful local commands:
just run
just test
just build
Without just:
go run ./cmd/server
go test ./...
go build -o .bin/update-server ./cmd/server
Local defaults use data-dev/ and http://127.0.0.1:8080 unless overridden by
environment variables.
More Docs
docs/API.md- current client API contract, examples, responses, and errors.docs/DEPLOYMENT.md- deployment, reverse proxy, backup, and restore notes.deploy/update-server.env.example- production environment example.deploy/Caddyfile.example- Caddy reverse proxy example.deploy/nginx.update-server.conf.example- Nginx reverse proxy example.example/autoupdate.py- minimal Python auto-update client helper.