Files
homelab/setup/apps-lxc.md
T
lerko96 cd454b2926 docs(public): populate phase 2 content
Full public/ directory — services, network, decisions, security,
inventory, rebuild sequence, and per-LXC setup guides. Sourced from
wiki. No secrets or WAN IPs included.
2026-04-17 21:23:59 -04:00

7.1 KiB

Apps LXC Setup

Overview

The apps LXC (10.2.0.60) in VLAN 1020 runs 15+ productivity apps via Docker Compose. All services run behind Authentik SSO (OIDC or forward auth). Shared infrastructure: single Postgres instance + single Redis instance, both local to the LXC. All services use network_mode: host to reach shared Postgres/Redis on localhost.

LXC Spec

Property Value
Hostname apps
IP 10.2.0.60/24
Gateway 10.2.0.1
DNS 10.2.0.11
Cores 4
RAM 6GB
Disk 80GB
Template debian-12-standard
Nesting ✓ (required for Docker)
Unprivileged

Services

Service Port Domain DB Auth
Outline 3000 outline.lerkolabs.com Postgres OIDC
Gitea 3001 gitea.lerkolabs.com Postgres OIDC
Vikunja 3456 tasks.lerkolabs.com Postgres OIDC
Ghostfolio 3333 finance.lerkolabs.com Postgres Forward auth
Hoarder 3002 hoarder.lerkolabs.com Postgres Forward auth
Grist 8484 grist.lerkolabs.com SQLite Forward auth
Glance 8080 glance.lerkolabs.com Forward auth
Actual Budget 5006 budget.lerkolabs.com File Forward auth
FreshRSS 8081 rss.lerkolabs.com SQLite Forward auth
Memos 5230 memos.lerkolabs.com SQLite Forward auth
Traggo 3030 time.lerkolabs.com SQLite Forward auth
Baikal 8082 dav.lerkolabs.com SQLite Forward auth
Filebrowser 8083 files.lerkolabs.com SQLite Forward auth
Bytestash 8084 SQLite Forward auth

Prerequisites

  • LXC created with nesting enabled before first start
  • Authentik OIDC providers created for Outline, Gitea, Vikunja before starting those services
  • Caddy Caddyfile updated with all service blocks (see Phase: Caddy)

Installation

apt update && apt upgrade -y
apt install -y curl wget git nano ufw
timedatectl set-timezone America/Chicago
curl -fsSL https://get.docker.com | sh
systemctl enable docker

Directory Structure

mkdir -p /opt/docker/apps/{shared,outline,gitea,vikunja,ghostfolio,hoarder,grist,glance,actual,freshrss,memos,traggo,baikal,filebrowser,bytestash}

Shared Infrastructure (Postgres + Redis)

Start this first, before anything else.

Shared .env

# /opt/docker/apps/.env
PG_ROOT_PASSWORD=      # openssl rand -base64 32
REDIS_PASSWORD=        # openssl rand -base64 24
PG_PASS_OUTLINE=       # openssl rand -base64 24
PG_PASS_GITEA=         # openssl rand -base64 24
PG_PASS_VIKUNJA=       # openssl rand -base64 24
PG_PASS_GHOSTFOLIO=    # openssl rand -base64 24
PG_PASS_HOARDER=       # openssl rand -base64 24
PG_PASS_GRIST=         # openssl rand -base64 24
chmod 600 /opt/docker/apps/.env

shared/docker-compose.yml

services:
  postgres:
    image: postgres:18-alpine
    container_name: apps-postgres
    restart: unless-stopped
    env_file: /opt/docker/apps/.env
    environment:
      POSTGRES_PASSWORD: ${PG_ROOT_PASSWORD}
      POSTGRES_USER: postgres
    volumes:
      - ./pgdata:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "127.0.0.1:5432:5432"
    networks:
      - apps-net

  redis:
    image: redis:7-alpine
    container_name: apps-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1 --loglevel warning
    env_file: /opt/docker/apps/.env
    volumes:
      - ./redisdata:/data
    ports:
      - "127.0.0.1:6379:6379"
    networks:
      - apps-net

networks:
  apps-net:
    name: apps-net
    driver: bridge

Database init script

mkdir -p /opt/docker/apps/shared/init

/opt/docker/apps/shared/init/01-create-databases.sql:

CREATE USER outline WITH PASSWORD 'PG_PASS_OUTLINE_VALUE';
CREATE DATABASE outline OWNER outline;

CREATE USER gitea WITH PASSWORD 'PG_PASS_GITEA_VALUE';
CREATE DATABASE gitea OWNER gitea;

CREATE USER vikunja WITH PASSWORD 'PG_PASS_VIKUNJA_VALUE';
CREATE DATABASE vikunja OWNER vikunja;

CREATE USER ghostfolio WITH PASSWORD 'PG_PASS_GHOSTFOLIO_VALUE';
CREATE DATABASE ghostfolio OWNER ghostfolio;

CREATE USER hoarder WITH PASSWORD 'PG_PASS_HOARDER_VALUE';
CREATE DATABASE hoarder OWNER hoarder;

CREATE USER grist WITH PASSWORD 'PG_PASS_GRIST_VALUE';
CREATE DATABASE grist OWNER grist;

Replace each _VALUE with the actual password from your .env. This script runs once on first docker compose up.

Start shared infrastructure

cd /opt/docker/apps/shared
docker compose --env-file /opt/docker/apps/.env up -d
docker compose logs -f  # wait for "ready to accept connections"

Verify

docker exec apps-postgres pg_isready -U postgres
docker exec apps-postgres psql -U postgres -c "\l"
# Should show: outline, gitea, vikunja, ghostfolio, hoarder, grist

Startup Order

# 1. Shared infrastructure always first
cd /opt/docker/apps/shared && docker compose up -d
sleep 15  # give postgres time on first run

# 2. Run Outline migrations before starting Outline
cd /opt/docker/apps/outline && docker compose run --rm outline-migrate
docker compose up -d outline

# 3. Everything else in parallel
for svc in gitea vikunja ghostfolio hoarder grist glance actual freshrss memos traggo baikal filebrowser bytestash; do
  cd /opt/docker/apps/$svc && docker compose up -d
done

Caddy Configuration

Add to Caddyfile on the infra LXC (10.2.0.20). Services with native OIDC don't need forward auth; others do.

# OIDC-native (no forward auth needed)
outline.lerkolabs.com {
    reverse_proxy 10.2.0.60:3000
}
gitea.lerkolabs.com {
    reverse_proxy 10.2.0.60:3001
}
tasks.lerkolabs.com {
    reverse_proxy 10.2.0.60:3456
}

# Forward auth
finance.lerkolabs.com {
    import authentik_forward_auth
    reverse_proxy 10.2.0.60:3333
}
# ... repeat pattern for remaining services

Pi-hole DNS Records

All records point to 10.2.0.20 (Caddy), not 10.2.0.60 directly:

outline.lerkolabs.com    → 10.2.0.20
gitea.lerkolabs.com      → 10.2.0.20
tasks.lerkolabs.com      → 10.2.0.20
finance.lerkolabs.com    → 10.2.0.20
hoarder.lerkolabs.com    → 10.2.0.20
grist.lerkolabs.com      → 10.2.0.20
glance.lerkolabs.com     → 10.2.0.20
budget.lerkolabs.com     → 10.2.0.20
rss.lerkolabs.com        → 10.2.0.20
memos.lerkolabs.com      → 10.2.0.20
time.lerkolabs.com       → 10.2.0.20
dav.lerkolabs.com        → 10.2.0.20
files.lerkolabs.com      → 10.2.0.20

Verification

# All containers running
docker ps --format "table {{.Names}}\t{{.Status}}" | sort

# Outline health
curl -s http://localhost:3000/api/health

# From LAN — check Authentik gate works
curl -I https://outline.lerkolabs.com

Useful Commands

# Logs for a service
docker logs -f outline

# Postgres: connect to a database
docker exec -it apps-postgres psql -U postgres -d outline

# Postgres: backup
docker exec apps-postgres pg_dump -U postgres outline > /opt/backups/outline-$(date +%Y%m%d).sql

# Disk usage by service
du -sh /opt/docker/apps/*/