> ## Content Index
> Fetch the complete content index at: https://blog.uptime.bet/llms.txt
> Use this file to discover other available public pages before exploring further.

# Plain Week: A Self-Hosted, Offline-First Weekly Planner in One Docker Container
- URL: https://blog.uptime.bet/plain-week-self-hosted-weekly-planner-docker/
- Published: 2026-11-11T08:27:32.000Z
- Updated: 2026-11-11T08:27:32.000Z
- Description: Plain Week is a minimal, MIT-licensed weekly planner that works offline and syncs through your own server. We tested version 0.3.15 with Docker Compose: one container, SQLite, about 130 MiB of RAM.
- Author: Alexandre Nebeling
- Tags: Trending, Self-Hosted, Open Source, Docker, Homelab, Productivity

A lot of self-hosted task managers come with projects, labels, Kanban boards and team permissions. Sometimes you just want to see your week on one screen and type into it. That's the whole idea behind [Plain Week](https://github.com/victor-falcon/plain-week?ref=blog.uptime.bet), a small MIT-licensed weekly planner that showed up as the featured project in [Self-Host Weekly on 6 November](https://selfh.st/weekly/2026-11-06/?ref=blog.uptime.bet) and has been shipping several releases a week.

Plain Week is offline-first. You can open it and start planning without an account, and your tasks live in the browser. If you want them on more than one device, you create an account on your own server and the app syncs through it. The self-hosted version is one container with SQLite, nothing else.

I set up version 0.3.15 with Docker Compose on a fresh Ubuntu 24.04 VM, clicked through the planner, created an account and checked that the tasks actually landed in the server database. Here's how that went.

## What Plain Week does

The main screen is the current week: Monday to Friday side by side, Saturday and Sunday stacked in the last column, and a "Someday" area at the bottom for things without a date. You click an empty row, type, hit Enter and keep typing. There are no forms in the way.

A few small parsing tricks make that fast. If a task starts with a time, like `09:00 Update the Proxmox host`, the time is pulled out and shown as a badge, and in my test timed tasks moved to the top of their day. Since 0.3.15 you can also start with a date: I typed `14 Nov 10:00 Farmers market` into Wednesday, and the task landed on Saturday at 10:00 with a small "Added to Sat 14 Nov" toast. According to the changelog, that date shortcut understands English and Spanish month names only.

Clicking a task opens a compact dialog with the date, time, notes, a subtask checklist, a repeat option and a color picker. Right-clicking a task gives you a context menu to change its color, push it to tomorrow, next week or Someday, duplicate it or delete it, and each entry shows its keyboard shortcut.

![Plain Week task dialog with a note and three subtasks](https://blog.uptime.bet/content/images/2026/11/plain-week-02-task-dialog.png)

The task dialog with notes and subtasks. Plain Week 0.3.15 running on our test VM (demo data). Screenshot: uptime.bet

Recurring tasks are supported (every N days, weekly on chosen weekdays, or monthly on a given day, with an optional end date). There's a light and a dark theme, the app installs as a PWA, and it keeps working without a connection because the data sits in IndexedDB.

Two integrations are optional and stay hidden until you configure them: a one-way sync of dated tasks into a dedicated Google Calendar, and browser push notifications (a morning summary plus a reminder five minutes before timed tasks). I didn't set up either for this test. With neither configured, the settings only showed Profile, Security, Appearance, Preferences and Data. The docs say the integration and notification pages stay hidden until their variables are set, and that's what I saw.

### Guest mode vs. account

This distinction matters for self-hosters. As a guest, everything stays in that one browser. Clear your site data and the tasks are gone. Once you create an account on your instance, the browser syncs with the server, and the JSON export and import under Settings › Data become available.

When I signed up in the test, the tasks I'd typed as a guest came along into the account, while the built-in tutorial tasks that a fresh install shows disappeared. A quick look into the SQLite file afterwards showed one user and all eight of my tasks on the server.

![Plain Week settings page for exporting and importing tasks as JSON](https://blog.uptime.bet/content/images/2026/11/plain-week-03-settings-data.png)

Settings › Data: JSON export and import are available once you have an account. Plain Week 0.3.15 on our test VM (demo data). Screenshot: uptime.bet

## What you need

- A Linux host with Docker and the Compose plugin. The image is built for `linux/amd64` and `linux/arm64`.
- Very little memory. The docs ask for about 256 MB of free memory and say the container uses about 125 MB at rest.
- For anything beyond `localhost`: a domain and a reverse proxy that terminates HTTPS. The offline mode relies on a service worker, and browsers only allow those over HTTPS or on `localhost`.

## Installing Plain Week with Docker Compose

### 1\. Create a folder and download the compose file

The project ships a compose file in its repository. Grab the one from the 0.3.15 tag so it matches the image you're about to run:

```bash
mkdir plain-week && cd plain-week
curl -fsSLO https://raw.githubusercontent.com/victor-falcon/plain-week/v0.3.15/deploy/docker-compose.yml
```

### 2\. Pin the version

The upstream file uses the `latest` tag. I prefer to update on purpose, so replace it with the release tag:

```bash
sed -i 's#ghcr.io/victor-falcon/plain-week:latest#ghcr.io/victor-falcon/plain-week:0.3.15#' docker-compose.yml
```

After that edit, the file (minus its comment header) looks like this:

```yaml
services:
    plainweek:
        image: ghcr.io/victor-falcon/plain-week:0.3.15
        restart: unless-stopped
        ports:
            - '127.0.0.1:8080:8080'
        volumes:
            - plainweek-storage:/app/storage
        environment:
            APP_KEY: ${APP_KEY:?Set APP_KEY in .env (see the comment at the top of docker-compose.yml)}
            APP_URL: ${APP_URL:-http://localhost:8080}
            APP_NOINDEX: ${APP_NOINDEX:-false}
            TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1,fc00::/7}
            MAIL_MAILER: ${MAIL_MAILER:-log}
            MAIL_SCHEME: ${MAIL_SCHEME:-}
            MAIL_HOST: ${MAIL_HOST:-}
            MAIL_PORT: ${MAIL_PORT:-587}
            MAIL_USERNAME: ${MAIL_USERNAME:-}
            MAIL_PASSWORD: ${MAIL_PASSWORD:-}
            MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-hello@example.com}
            MAIL_FROM_NAME: ${MAIL_FROM_NAME:-Plain Week}
volumes:
    plainweek-storage:
```

Note that the port is already bound to `127.0.0.1`. That's a sensible default: the app speaks plain HTTP on port 8080 and is meant to sit behind a reverse proxy.

### 3\. Generate the app key and set the URL

`APP_KEY` is the only required setting. It encrypts sessions and cookies, so generate it once and keep it. Changing it later signs everyone out. The container refuses to start without a valid key.

```bash
echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env
echo "APP_URL=http://localhost:8080" >> .env
chmod 600 .env
```

For a public instance, set `APP_URL` to the real address instead, for example `https://plan.example.com`. With an `https://` URL, the session cookie is marked secure automatically.

### 4\. Start it

```bash
docker compose up -d
docker compose ps
```

On first start, the entrypoint creates the SQLite database and runs the migrations, then FrankenPHP serves the app on port 8080 as an unprivileged user (uid 1000). The image has a built-in health check against `/up`. In my test, the container went from nothing to "healthy" in under 20 seconds, including pulling the image. Open `http://localhost:8080` (or tunnel to it over SSH) and you'll see the week view with a few tutorial tasks.

### 5\. Put it behind a reverse proxy

Plain Week needs to live at the root of its domain, not under a sub-path, because the service worker and the offline shell assume `/`. With Caddy, the docs' example is as short as it gets:

```
plan.example.com {
	reverse_proxy 127.0.0.1:8080
}
```

Your proxy needs to pass `X-Forwarded-Proto` and `X-Forwarded-Host`. Caddy and Traefik do that by default; with nginx you add the two `proxy_set_header` lines yourself. The container trusts these headers from loopback and private networks unless you change `TRUSTED_PROXIES`.

## Resource usage in practice

Right after the container turned healthy, `docker stats` showed about 129 MiB of RAM and near-zero CPU. After I'd clicked through the planner, signed up and added tasks, it sat at about 164 MiB. That's one container running the web server, the Laravel scheduler and a queue worker side by side, so there's nothing else to add.

The image itself is less tiny. Docker reported roughly 1.1 GB for the unpacked image, which is a lot for a to-do app if you're tight on disk.

![Plain Week week view in dark mode with demo tasks](https://blog.uptime.bet/content/images/2026/11/plain-week-04-week-dark.png)

Dark mode follows the system setting. Plain Week 0.3.15 on our test VM (demo data). Screenshot: uptime.bet

## Tips from the test

**Set `APP_NOINDEX=true`** on a public instance. It adds a `noindex` header and meta tag and makes `/robots.txt` disallow all crawlers. There's no reason for search engines to see your planner's login page.

**Configure SMTP if more than one person uses it.** By default, emails go to the container logs (`MAIL_MAILER=log`), so password reset links never reach anyone. The app still works without mail. My demo account showed an "email unverified" notice, but nothing was blocked.

**Notifications and Google Calendar need extra variables.** The compose file in 0.3.15 doesn't pass the VAPID or Google variables to the container. If you want push notifications, you'll have to add `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` and `VAPID_SUBJECT` to the `environment` block yourself (and `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` for the calendar sync). The self-hosting guide explains how to generate the keys.

**Back up the volume.** Everything the server stores sits in one SQLite file under `/app/storage`. The docs describe an online backup with SQLite's backup API via `docker exec` and, alternatively, stopping the container and tarring the volume. The docs' commands use a container called `plainweek`. With Compose, the container gets a generated name such as `plain-week-plainweek-1` (check with `docker compose ps`), and the volume gets the project prefix, for example `plain-week_plainweek-storage`. Adjust the names before you copy the commands.

**Updating** is the usual `docker compose pull && docker compose up -d` after changing the tag. Migrations run automatically on start, so back up first.

## Limits and security notes

Plain Week is young. The repository was created in mid-October 2026, it's still at version 0.x, and releases come out at a pace of several per week. That's great for features, but expect changes, and don't skip backups before updates.

Sign-up is open. In 0.3.15 I didn't find a setting in the docs or the shipped config to disable registration, so anyone who can reach your instance can create an account. For a personal planner, I'd keep it behind a VPN, Tailscale or an authenticating reverse proxy rather than exposing it to the whole internet.

Guest data is fragile. Without an account, the tasks only exist in one browser's storage, and the JSON export is tied to having an account. If you use Plain Week seriously, create an account even if you're the only user.

It's a personal tool. I didn't see shared lists, team features or project hierarchies, and that seems to be by design. The codebase also contains the billing code for the hosted plainweek.eu service, but on a self-hosted instance `SAAS_ENABLED` defaults to `false`, which means accounts are free, sync has no limits and the billing routes return 404.

## Plain Week vs. the alternatives

On GitHub, the project tags itself as a Tweek alternative, so it's aimed at people who like that week-on-one-page style of planning. Among self-hosted options, these are the obvious comparisons:

- [Will Be Done](https://github.com/will-be-done/will-be-done?ref=blog.uptime.bet) (AGPL-3.0) is the closest in spirit: a local-first task manager built around a visual weekly timeline, with SQLite and a single Docker command.
- [Vikunja](https://github.com/go-vikunja/vikunja?ref=blog.uptime.bet) (AGPL-3.0) is a full task manager with projects and subprojects, list, Gantt, table and Kanban views, sharing with users and teams, and CalDAV. More powerful, but also a lot more than a weekly planner.
- [Super Productivity](https://github.com/super-productivity/super-productivity?ref=blog.uptime.bet) (MIT) focuses on timeboxing and time tracking and integrates with Jira, GitLab, GitHub and OpenProject.

If you want structure, Vikunja is the safer bet. If you want the week on one screen and nothing else, Plain Week is the smaller tool for that job.

## Conclusion

Plain Week covers weekly planning and leaves everything else out. In my test, fast entry with time and date shortcuts worked as described, sync to my own server worked on the first try, and the single container stayed well under 200 MiB of RAM. The open registration and the project's young age are the two things to keep in mind, and both are easy to live with if you keep the instance private and back up the SQLite volume.

If you already run a dashboard like [Homepage](https://blog.uptime.bet/homepage-dashboard-homelab-docker/), add a link to your Plain Week instance there so your week is one click away.

## Sources

- [Plain Week on GitHub (README)](https://github.com/victor-falcon/plain-week?ref=blog.uptime.bet)
- [Plain Week v0.3.15 release](https://github.com/victor-falcon/plain-week/releases/tag/v0.3.15?ref=blog.uptime.bet)
- [Plain Week changelog (v0.3.15)](https://github.com/victor-falcon/plain-week/blob/v0.3.15/CHANGELOG.md?ref=blog.uptime.bet)
- [Plain Week self-hosting guide (v0.3.15)](https://github.com/victor-falcon/plain-week/blob/v0.3.15/docs/self-hosting.md?ref=blog.uptime.bet)
- [Plain Week docker-compose.yml (v0.3.15)](https://github.com/victor-falcon/plain-week/blob/v0.3.15/deploy/docker-compose.yml?ref=blog.uptime.bet)
- [Plain Week license (MIT)](https://github.com/victor-falcon/plain-week/blob/v0.3.15/LICENSE?ref=blog.uptime.bet)
- [Plain Week container image on GHCR](https://github.com/victor-falcon/plain-week/pkgs/container/plain-week?ref=blog.uptime.bet)
- [Self-Host Weekly, 6 November 2026](https://selfh.st/weekly/2026-11-06/?ref=blog.uptime.bet)
- [Will Be Done on GitHub](https://github.com/will-be-done/will-be-done?ref=blog.uptime.bet)
- [Vikunja on GitHub](https://github.com/go-vikunja/vikunja?ref=blog.uptime.bet) and [Vikunja features](https://vikunja.io/features/?ref=blog.uptime.bet)
- [Super Productivity on GitHub](https://github.com/super-productivity/super-productivity?ref=blog.uptime.bet)