Self-hosting
Configuration
Environment variables, volumes, ports and image tags for a self-hosted Inkspace.
Inkspace is configured entirely through environment variables on the two Compose services. Changes made inside a running container are not persistent — keep all configuration in your Compose file.
App service
| Variable | Required | Default | Notes |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string, e.g. postgresql://inkspace:PASSWORD@db:5432/inkspace. |
BETTER_AUTH_URL |
Yes | — | Public site URL, e.g. https://stories.example.com. Must match the browser origin. |
BETTER_AUTH_SECRET |
Yes | — | Long, stable random secret. Changing it can invalidate sessions. |
PORT |
No | 6767 |
Port the app listens on inside the container. |
INKSPACE_UPLOAD_DIR |
No | /inkspace/uploads |
Directory for uploaded images. |
INKSPACE_ALLOW_SCHEMA_DATA_LOSS |
No | unset | Set to true for one start to let a schema update drop tables or columns. Back up first. |
The container refuses to start if DATABASE_URL, BETTER_AUTH_URL or
BETTER_AUTH_SECRET is missing.
Generate a secret with:
openssl rand -base64 32
Why BETTER_AUTH_URL matters
Inkspace’s write endpoints — authentication, uploads, import and error logging — reject
requests whose Origin doesn’t match BETTER_AUTH_URL. Set it to exactly what people
type in the browser, including the scheme and any non-default port:
| You open Inkspace at | Set BETTER_AUTH_URL to |
|---|---|
http://localhost:6767 |
http://localhost:6767 |
http://192.168.1.20:6767 |
http://192.168.1.20:6767 |
https://stories.example.com |
https://stories.example.com |
Database service
Use the official postgres:16-alpine image.
| Variable | Notes |
|---|---|
POSTGRES_USER |
Database user. |
POSTGRES_PASSWORD |
Use a strong, stable password. |
POSTGRES_DB |
Database name, usually inkspace. |
Don’t publish the database port to the internet. In the example Compose file only the
app is reachable from outside; it talks to db over the internal Compose network.
Persistent data
Mount both of these paths on persistent storage:
| Path | Container | Stores |
|---|---|---|
/var/lib/postgresql/data |
db |
Users, stories, comments, likes, ratings, notifications, settings |
/inkspace/uploads |
app |
Avatars, story covers, story images and comment images |
The app runs as the unprivileged node user (UID 1000). A bind-mounted uploads
folder must be writable by that user:
sudo chown -R 1000:1000 uploads
Careful with
down -v.docker compose down -vdeletes Compose-managed volumes, including your database. Only use it when you really want to wipe everything.
Image tags
Images are published to ghcr.io/ak4duy/inkspace:
| Tag | Built from | Use it for |
|---|---|---|
latest |
master branch |
Real installs. Recommended. |
nightly |
nightly branch |
Trying new features early. |
X.Y.Z, X.Y |
Release tags such as v1.2.3 |
Pinning an exact release. |
sha-<commit> |
Any published commit | Reproducing a specific build. |
The running version, branch and build date are shown in Settings → Inkspace.
Upload limits
| Upload | Formats | Max size |
|---|---|---|
| Avatar | JPEG, PNG, WebP | 2 MB |
| Story cover | JPEG, PNG, WebP, GIF | 10 MB |
| Story image | JPEG, PNG, WebP, GIF | 10 MB |
| Comment image | JPEG, PNG, WebP, GIF | 10 MB |
| Data import | Inkspace export JSON | 25 MB |
Files are checked by their actual file signature, not just their extension. Uploads that are never attached to a story or comment are cleaned up automatically after a day.
Security headers
Every response carries X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
Referrer-Policy: strict-origin-when-cross-origin and a Permissions-Policy that
disables camera, microphone and geolocation. You don’t need to add these at your proxy.