Skip to content

Apps & Collaboration

🛠️ The Apps I Actually Use

These are the stacks I keep coming back to because they feel like real services, not just containers with a port exposed.

They are where storage, URLs, background jobs, login, and database choices start mattering all at once, which is exactly why I am documenting them.

☁️ Nextcloud

Nextcloud Logo

Personal Cloud

Nextcloud is one of those tools I like because it stops being "just a file app" pretty quickly. The useful part for me is not only sync and sharing, but the way it forces a cleaner setup around storage, database, cache, proxying, and background jobs.

Config

This stack splits the web app and background cron work into separate containers. That keeps scheduled maintenance out of the main request path. It also wires in trusted domain settings, reverse-proxy hints, larger PHP limits, and external Postgres and Redis connections.

Compose

name: nextcloud

networks:
  nextcloud:

services:
  nextcloud:
    container_name: nextcloud
    image: nextcloud:${NEXTCLOUD_VERSION:-stable}
    volumes:
      - ${NEXTCLOUD_DATA_PATH}:/var/www/html
    networks:
      - nextcloud
    ports:
      - ${NEXTCLOUD_PORT:-8080}:80
    environment:
      NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_URL:-localhost}
      TRUSTED_PROXIES: ${TRUSTED_PROXIES}
      OVERWRITEPROTOCOL: ${PROTOCOL:-https}
      OVERWRITECLIURL: ${PROTOCOL:-https}://${NEXTCLOUD_URL:-localhost}
      PHP_VALUE: "session.save_handler=redis\nsession.save_path='tcp://${REDIS_HOST}:${REDIS_PORT:-6379}?auth=${REDIS_PASS}&database=0'"
      PHP_UPLOAD_LIMIT: 5G
      PHP_MEMORY_LIMIT: 2G
      NC_allow_local_remote_servers: 'true'
      POSTGRES_HOST: ${POSTGRESQL_HOST:?POSTGRESQL_HOST; no database host}
      POSTGRES_HOST_PORT: ${POSTGRESQL_PORT:-5432}
      POSTGRES_DB: ${NEXTCLOUD_PG_DB:?NEXTCLOUD_PG_DB; no database name}
      POSTGRES_USER: ${NEXTCLOUD_PG_USER:?NEXTCLOUD_PG_USER; no database user}
      POSTGRES_PASSWORD: ${NEXTCLOUD_PG_PASS:?NEXTCLOUD_PG_PASS; no database password}
      REDIS_HOST: ${REDIS_HOST:?REDIS_HOST; no redis host}
      REDIS_HOST_PORT: ${REDIS_PORT:-6379}
      REDIS_HOST_PASSWORD: ${REDIS_PASS}
    restart: unless-stopped

  nextcloud-cron:
    container_name: nextcloud-cron
    image: nextcloud:${NEXTCLOUD_VERSION:-stable}
    volumes:
      - ${NEXTCLOUD_DATA_PATH}:/var/www/html
    networks:
      - nextcloud
    entrypoint: /cron.sh
    environment:
      NEXTCLOUD_CRON_PERIOD: 10m
      POSTGRES_HOST: ${POSTGRESQL_HOST:?POSTGRESQL_HOST; no database host}
      POSTGRES_HOST_PORT: ${POSTGRESQL_PORT:-5432}
      POSTGRES_DB: ${NEXTCLOUD_PG_DB:?NEXTCLOUD_PG_DB; no database name}
      POSTGRES_USER: ${NEXTCLOUD_PG_USER:?NEXTCLOUD_PG_USER; no database user}
      POSTGRES_PASSWORD: ${NEXTCLOUD_PG_PASS:?NEXTCLOUD_PG_PASS; no database password}
      REDIS_HOST: ${REDIS_HOST:?REDIS_HOST; no redis host}
      REDIS_HOST_PORT: ${REDIS_PORT:-6379}
      REDIS_HOST_PASSWORD: ${REDIS_PASS}
    restart: unless-stopped

Example env

NEXTCLOUD_VERSION=stable
NEXTCLOUD_PORT=8080
NEXTCLOUD_URL=nextcloud.example.com
PROTOCOL=https
TRUSTED_PROXIES="192.168.0.0/16"
NEXTCLOUD_DATA_PATH="/srv/nextcloud"
POSTGRESQL_HOST=postgresql
POSTGRESQL_PORT=5432
NEXTCLOUD_PG_DB=nextcloud
NEXTCLOUD_PG_USER=nextcloud
NEXTCLOUD_PG_PASS=change-me
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASS=change-me

Vars

Variable Purpose Why it matters
NEXTCLOUD_VERSION Image tag Pin when you want predictable upgrades
NEXTCLOUD_PORT Host port Used when not behind a proxy
NEXTCLOUD_URL Public hostname Needed for trusted domains and generated links
PROTOCOL URL scheme Important behind HTTPS reverse proxies
TRUSTED_PROXIES Proxy IP ranges Helps Nextcloud trust the real client path
NEXTCLOUD_DATA_PATH App data path Main persistent app mount
POSTGRESQL_HOST / POSTGRESQL_PORT Database endpoint Nextcloud stores metadata there
NEXTCLOUD_PG_DB / NEXTCLOUD_PG_USER / NEXTCLOUD_PG_PASS Database credentials Required for startup
REDIS_HOST / REDIS_PORT / REDIS_PASS Redis endpoint Used for locks, sessions, and cache

🐙 Gitea

Gitea Logo

Self-Hosted Git

Gitea is the kind of service I like showing because it looks simple on the surface, but the good version of it has proper database, cache, mail, and URL settings underneath. That makes it a nice "small app, real platform thinking" example.

Config

The container stores everything under /data, exposes both HTTP and SSH, and configures database, cache, sessions, queues, and mail entirely through environment variables. Redis is used three times here, which is exactly the sort of detail that makes the stack feel properly wired instead of half-finished.

Compose

name: gitea

services:
  server:
    image: gitea/gitea:${GITEA_VERSION:-latest}
    container_name: gitea
    ports:
      - ${GITEA_PORT:-3000}:3000
      - ${GITEA_PORT_SSH:-2221}:${GITEA_PORT_SSH:-2221}
    volumes:
      - ${GITEA_DATA_PATH:-gitea}:/data
    environment:
      TZ: ${TZ:-Etc/UTC}
      USER_UID: ${UID:-1000}
      USER_GID: ${GID:-1000}
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: ${POSTGRESQL_HOST:?POSTGRESQL_HOST; no database host}:${POSTGRESQL_PORT:-5432}
      GITEA__database__NAME: ${GITEA_PG_DB:?GITEA_PG_DB; no database name}
      GITEA__database__USER: ${GITEA_PG_USER:?GITEA_PG_USER; no database user}
      GITEA__database__PASSWD: ${GITEA_PG_PASS:?GITEA_PG_PASS; no database password}
      GITEA__cache__ADAPTER: redis
      GITEA__cache__HOST: redis://${REDIS_PASS}@${REDIS_HOST}:${REDIS_PORT:-6379}/${REDIS_DBID}
      GITEA__session__PROVIDER: redis
      GITEA__session__PROVIDER_CONFIG: redis://${REDIS_PASS}@${REDIS_HOST}:${REDIS_PORT:-6379}/${REDIS_DBID}
      GITEA__queue__TYPE: redis
      GITEA__queue__CONN_STR: redis://${REDIS_PASS}@${REDIS_HOST}:${REDIS_PORT:-6379}/${REDIS_DBID}
      GITEA__mailer__ENABLED: "true"
      GITEA__mailer__PROTOCOL: smtp+starttls
      GITEA__mailer__FROM: ${EMAIL_FROM}
      GITEA__mailer__SMTP_ADDR: ${EMAIL_HOST}
      GITEA__mailer__SMTP_PORT: ${EMAIL_PORT}
      GITEA__mailer__USER: ${EMAIL_USER}
      GITEA__mailer__PASSWD: ${EMAIL_PASS}
      DOMAIN: ${GITEA_DOMAIN:-localhost}
      ROOT_URL: ${GITEA_ROOT_URL:-https://${GITEA_DOMAIN:-localhost}:${GITEA_PORT:-3000}/}
      SSH_DOMAIN: ${GITEA_DOMAIN:-localhost}
      SSH_PORT: ${GITEA_PORT_SSH:-2221}
      USE_COMPAT_SSH_URI: ${GITEA_COMPAT_SSH_URI:-false}
    restart: unless-stopped

Example env

UID=1000
GID=1000
TZ="Asia/Tokyo"
GITEA_VERSION=latest
GITEA_PORT=3000
GITEA_PORT_SSH=2221
GITEA_DOMAIN=git.example.com
GITEA_ROOT_URL="https://git.example.com/"
GITEA_DATA_PATH="/srv/gitea"
POSTGRESQL_HOST=postgresql
POSTGRESQL_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DBID=0
REDIS_PASS=""
GITEA_PG_DB=gitea
GITEA_PG_USER=gitea
GITEA_PG_PASS=change-me
EMAIL_FROM="Gitea <noreply@example.com>"
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=user@example.com
EMAIL_PASS=app-password

Vars

Variable Purpose Why it matters
GITEA_VERSION Image tag Pinning avoids surprise changes
GITEA_PORT / GITEA_PORT_SSH HTTP and SSH ports Needed for web access and clone URLs
GITEA_DOMAIN / GITEA_ROOT_URL Public URL settings Keeps links and callbacks correct
GITEA_DATA_PATH Persistent data path Repositories and app state live here
POSTGRESQL_HOST / POSTGRESQL_PORT Database endpoint Main app database
GITEA_PG_DB / GITEA_PG_USER / GITEA_PG_PASS Database credentials Required for startup
REDIS_HOST / REDIS_PORT / REDIS_DBID / REDIS_PASS Redis connection Powers cache, sessions, and queues
EMAIL_FROM / EMAIL_HOST / EMAIL_PORT / EMAIL_USER / EMAIL_PASS SMTP settings Needed for notifications and resets
UID / GID / TZ Host compatibility Helps with file ownership and timestamps

📘 Wiki.js

Wiki.js Logo

Docs Home

Wiki.js is one I like for the opposite reason from some of the bigger stacks: it stays pretty lean, but still benefits a lot from being wired to a real Postgres backend instead of pretending persistence does not matter.

Config

This recipe is intentionally lean: one app container, one exposed port, and a PostgreSQL connection defined through environment variables. It is easy to drop into an existing shared-database setup.

Compose

name: wikijs

services:
  wikijs:
    image: requarks/wiki:${WIKIJS_VERSION:-latest}
    container_name: wikijs
    ports:
      - ${WIKIJS_PORT:-3000}:3000
    environment:
      DB_TYPE: postgres
      DB_HOST: ${POSTGRESQL_HOST:?POSTGRESQL_HOST; no database host}
      DB_PORT: ${POSTGRESQL_PORT:-5432}
      DB_NAME: ${WIKIJS_PG_DB:?WIKIJS_PG_DB; no database name}
      DB_USER: ${WIKIJS_PG_USER:?WIKIJS_PG_USER; no database user}
      DB_PASS: ${WIKIJS_PG_PASS:?WIKIJS_PG_PASS; no database password}
    restart: unless-stopped

Example env

WIKIJS_VERSION=latest
WIKIJS_PORT=3000
POSTGRESQL_HOST=postgresql
POSTGRESQL_PORT=5432
WIKIJS_PG_DB=wikijs
WIKIJS_PG_USER=wikijs
WIKIJS_PG_PASS=change-me

Vars

Variable Purpose Why it matters
WIKIJS_VERSION Image tag Pinning is still a good habit
WIKIJS_PORT Host port Default app access port
POSTGRESQL_HOST / POSTGRESQL_PORT Database endpoint Wiki.js needs Postgres to boot
WIKIJS_PG_DB / WIKIJS_PG_USER / WIKIJS_PG_PASS Database credentials Core connection settings

🔐 Authentik

Authentik Logo

Single Sign-On

Authentik is the service that makes the rest of the stack feel like one environment instead of a pile of unrelated logins. I like keeping it here because it shows the jump from "app container" to "shared identity layer."

Config

The recipe includes Postgres, Redis, a dedicated server container, and a separate worker container. That server/worker split is the part I like most here. It shows the service is being run the way it expects to be run, not squeezed into a single process just because it is possible.

Compose

name: authentik

volumes:
  postgresql:
    name: ${POSTGRES_VOLUME:-postgresql}
  redis:
    name: ${REDIS_VOLUME:-redis}
  authentik-templates:
    name: ${AUTHENTIK_VOLUME_TEMPLATES:-authentik_templates}
  authentik-certs:
    name: ${AUTHENTIK_VOLUME_CERTS:-authentik_certs}

networks:
  authentik:

services:
  postgresql:
    image: postgres:${POSTGRES_VERSION:-17-alpine}
    container_name: postgresql
    networks:
      - authentik
    volumes:
      - postgresql:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${AUTHENTIK_PG_DB:-authentik}
      POSTGRES_USER: ${AUTHENTIK_PG_USER:-authentik}
      POSTGRES_PASSWORD: ${AUTHENTIK_PG_PASS}
    healthcheck:
      interval: 30s
      start_period: 20s
      timeout: 10s
      retries: 5
      test:
        - CMD-SHELL
        - pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
    mem_reservation: 256mb
    restart: always

  redis:
    image: redis:${REDIS_VERSION:-alpine}
    container_name: redis
    networks:
      - authentik
    volumes:
      - redis:/data
    command: --save 60 1 --loglevel warning
    healthcheck:
      interval: 30s
      start_period: 20s
      timeout: 10s
      retries: 5
      test:
        - CMD-SHELL
        - redis-cli ping | grep PONG
    mem_reservation: 32mb
    restart: always

  authentik-server:
    image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2025.10}
    container_name: authentik-server
    command: server
    networks:
      - authentik
    ports:
      - ${AUTHENTIK_PORT:-9443}:9443
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__PORT: 5432
      AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_REDIS__PORT: 6379
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_EMAIL__FROM: ${EMAIL_FROM}
      AUTHENTIK_EMAIL__HOST: ${EMAIL_HOST}
      AUTHENTIK_EMAIL__PORT: ${EMAIL_PORT}
      AUTHENTIK_EMAIL__USERNAME: ${EMAIL_USER}
      AUTHENTIK_EMAIL__PASSWORD: ${EMAIL_PASS}
      AUTHENTIK_EMAIL__TIMEOUT: 30
      AUTHENTIK_EMAIL__USE_TLS: true
      AUTHENTIK_EMAIL__USE_SSL: false
    volumes:
      - authentik-templates:/templates
    mem_reservation: 384mb
    restart: always

  authentik-worker:
    image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2025.10}
    container_name: authentik-worker
    command: worker
    networks:
      - authentik
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__PORT: 5432
      AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_REDIS__PORT: 6379
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_EMAIL__FROM: ${EMAIL_FROM}
      AUTHENTIK_EMAIL__HOST: ${EMAIL_HOST}
      AUTHENTIK_EMAIL__PORT: ${EMAIL_PORT}
      AUTHENTIK_EMAIL__USERNAME: ${EMAIL_USER}
      AUTHENTIK_EMAIL__PASSWORD: ${EMAIL_PASS}
      AUTHENTIK_EMAIL__TIMEOUT: 30
      AUTHENTIK_EMAIL__USE_TLS: true
      AUTHENTIK_EMAIL__USE_SSL: false
    user: root
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - authentik-certs:/certs:z
      - authentik-templates:/templates
    mem_reservation: 192mb
    restart: always

Example env

AUTHENTIK_VERSION=2025.10
AUTHENTIK_PORT=9443
POSTGRES_VERSION=17-alpine
REDIS_VERSION=alpine
AUTHENTIK_PG_DB=authentik
AUTHENTIK_PG_USER=authentik
AUTHENTIK_PG_PASS=change-me
AUTHENTIK_SECRET_KEY=change-me
EMAIL_FROM="Authentik <noreply@example.com>"
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=user@example.com
EMAIL_PASS=app-password
POSTGRES_VOLUME=postgresql
REDIS_VOLUME=redis
AUTHENTIK_VOLUME_TEMPLATES=authentik_templates
AUTHENTIK_VOLUME_CERTS=authentik_certs

Vars

Variable Purpose Why it matters
AUTHENTIK_VERSION Image tag Worth pinning because auth stacks are sensitive to upgrades
AUTHENTIK_PORT Public port Main user-facing access point
AUTHENTIK_PG_DB / AUTHENTIK_PG_USER / AUTHENTIK_PG_PASS Postgres settings Core database connection
AUTHENTIK_SECRET_KEY App secret Critical security setting
POSTGRES_VERSION / REDIS_VERSION Dependency versions Useful when standardizing the stack
EMAIL_FROM / EMAIL_HOST / EMAIL_PORT / EMAIL_USER / EMAIL_PASS SMTP settings Needed for account-related mail
POSTGRES_VOLUME / REDIS_VOLUME Data volumes Keep backing services persistent
AUTHENTIK_VOLUME_TEMPLATES / AUTHENTIK_VOLUME_CERTS App volumes Keep templates and certificates

Comments