Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ WORKER_DELAY_SECONDS=3600
REPORTED_LOG_LEVEL=warn
LOG_APPENDER=println
# SENTRY_DSN= only needed when LOG_APPENDER=sentry

# Ring secure-site-defaults (recommended behind HTTPS/nginx)
SECURE_DEFAULTS=true
26 changes: 15 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ jobs:
--health-retries 5

env:
LEIN_ROOT: "true"
JVM_OPTS: -Xmx2g
DATABASE_URL: jdbc:postgresql://localhost:5432/self-destruct-test?user=selfdestruct&password=selfdestruct
DATABASE_ENCRYPTION_KEY: changemedbkey2
Expand All @@ -47,25 +46,30 @@ jobs:
distribution: temurin
java-version: "21"

- name: Set up Clojure / Leiningen
- name: Set up Clojure CLI
uses: DeLaGuardo/setup-clojure@13.6.1
with:
# 2.13.0 currently fails to install via setup-clojure; stay on 2.12.x
lein: 2.12.0
cli: 1.12.5.1664

- name: Cache Maven dependencies
- name: Cache Maven / Clojure deps
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('project.clj') }}
path: |
~/.m2/repository
~/.gitlibs
.cpcache
key: ${{ runner.os }}-clj-${{ hashFiles('deps.edn') }}
restore-keys: |
${{ runner.os }}-m2-
${{ runner.os }}-clj-

- name: Install dependencies
run: lein deps
run: clojure -P -M:test:dev:build

- name: Run database migrations
run: lein migrate
run: clojure -M:migrate

- name: Run tests
run: lein test
run: clojure -M:test

- name: Build uberjar
run: clojure -T:build uber
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
/checkouts/
/resources/public/css/
.cpcache/
.lein-*
.nrepl-port
*.jar
*.class
pom.xml
pom.xml.asc
profiles.clj
.env
64 changes: 27 additions & 37 deletions DEPLOYMENT_FREEBSD.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
# self-destruct FreeBSD Deployment

Deploy self-destruct on a FreeBSD home server using the same pattern as
DoThisWeek: dedicated app user, PostgreSQL, `.env`, Leiningen uberjar, `rc.d`
service, nginx reverse proxy, certbot, and daily DB backups.
Deploy self-destruct on FreeBSD with a dedicated app user, PostgreSQL, `.env`,
Clojure CLI uberjar, `rc.d` service, nginx reverse proxy, certbot, and daily DB
backups.

## Assumptions

- PostgreSQL, nginx, certbot, firewall rules, and (optionally) Cloudflare DDNS
are already available on the server.
- PostgreSQL, nginx, certbot, and basic firewall rules are available (or you will
install them as you go).
- `sudo` is available.
- The server has outbound HTTPS for GitHub, Maven/Clojars, Cloudflare, and
Let's Encrypt.
- The server has outbound HTTPS for GitHub, Maven/Clojars, and Let's Encrypt.
- App path is `/home/selfdestruct/self-destruct`.
- App user is `selfdestruct`.
- App port is `4003` (DoThisWeek uses `4002`).
- App port is `4003`.

## 1. Install Java Runtime and Build Tools

```sh
sudo pkg install openjdk21 git
sudo pkg install openjdk21 git clojure
```

Install Leiningen if it is not already on the system (e.g. `sudo pkg install
leiningen`, or place a `lein` script on `PATH`).

Verify:

```sh
/usr/local/openjdk21/bin/java -version
lein version
clj -Sdescribe
```

The deployment scripts force `JAVA_HOME=/usr/local/openjdk21` and put
Expand Down Expand Up @@ -78,7 +74,7 @@ psql -U selfdestruct -h localhost selfdestruct_prod
```

If needed, update `pg_hba.conf` to allow `md5` or `scram-sha-256`
authentication for localhost connections, matching the other deployed apps.
authentication for localhost TCP connections.

## 4. Create Production Environment

Expand Down Expand Up @@ -109,8 +105,8 @@ sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/bui

This:

- Fetches dependencies with `lein deps`
- Builds `target/self-destruct.jar` with `lein uberjar`
- Fetches dependencies with `clojure -P`
- Builds `target/self-destruct.jar` with `clojure -T:build uber`
- Runs migrations with `java -jar target/self-destruct.jar --migrate`

Optional local server test from the FreeBSD host:
Expand Down Expand Up @@ -171,29 +167,20 @@ sudo sysrc nginx_enable="YES"
sudo service nginx start
```

## 8. Configure DNS and Cloudflare DDNS
## 8. Configure DNS

In Cloudflare (or your DNS provider):
At your DNS provider:

- Create an A record for `@` pointing to the server public IP
- Create an A (or AAAA) record for the apex hostname pointing at the server
- Prefer a CNAME for `www` pointing at the apex hostname
- Keep records DNS-only unless you intentionally want a CDN proxy

If you already run a shared Cloudflare DDNS script on this host, add another
zone entry for this domain (same pattern as DoThisWeek / other apps). Example:

```sh
sudo sysrc cloudflare_ddns_zoneN_id="YOUR_ZONE_ID"
sudo sysrc cloudflare_ddns_zoneN_record_id="YOUR_RECORD_ID"
sudo sysrc cloudflare_ddns_zoneN_name="your.domain.example"
```
- If your DNS provider offers a CDN/proxy toggle (e.g. Cloudflare orange-cloud),
keep records DNS-only until certificates are issued, unless you intentionally
want that proxy in front of nginx

Then update the DDNS script to call `process_zone` for the new zone and verify:

```sh
sudo /usr/local/bin/cloudflare-ddns.sh
tail /var/log/cloudflare-ddns.log
```
If the server has a dynamic public IP, configure DDNS so the apex record stays
current before requesting certificates. If `www` is an A/AAAA record instead of
a CNAME, manage that record with DDNS too — otherwise it can go stale when the
IP changes.

## 9. Configure HTTPS

Expand All @@ -211,12 +198,15 @@ Verify HTTPS:
curl -I https://your.domain.example
```

Certbot renewal should already be configured if other apps use it. Verify:
Confirm certbot renewal is scheduled (or set it up), then dry-run:

```sh
sudo certbot renew --dry-run
```

Production `.env` sets `SECURE_DEFAULTS=true`, so prefer finishing HTTPS before
relying on browser sessions over plain HTTP.

## 10. Configure Database Backups

Run as root:
Expand Down Expand Up @@ -296,5 +286,5 @@ curl -s https://your.domain.example/health
| Backup log | /var/log/selfdestruct-backup.log |
| Backup time | 3:45 AM daily |
| Backup retention | 30 days |
| Build | `lein uberjar` |
| Build | `clojure -T:build uber` |
| Migrate | `java -jar target/self-destruct.jar --migrate` |
55 changes: 33 additions & 22 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
** Prerequisites

- [[https://adoptium.net/][Java]] 17 or newer (21 recommended; required by Ring / Jetty 12)
- [[https://github.com/technomancy/leiningen][Leiningen]] 2.11.0 or newer (2.12.x recommended)
- [[https://clojure.org/guides/install_clojure][Clojure CLI]] (~clj~ / ~clojure~)
- PostgreSQL available over TCP with a role/password your JDBC URL can use


** Development Mode

*** Set Environment Variables & Secrets

Copy the example file, then edit values to match your machine:
Copy the local example, then edit values to match your machine:

#+BEGIN_SRC sh
cp profiles_example.clj profiles.clj
cp dev.env.example .env
#+END_SRC

~profiles.clj~ is gitignored. The example includes local defaults for:
~.env~ is gitignored. On startup the app loads it automatically for any
variables not already set in the process environment (so exported env vars
and CI/production still win). The example includes:

- ~DATABASE_URL~ — JDBC URL with user/password (not Unix peer auth)
- ~DATABASE_ENCRYPTION_KEY~ — key used to encrypt messages at rest
Expand All @@ -28,66 +30,75 @@

*** Setup Local Development Database

Create a Postgres role and database that match ~profiles.clj~ (example uses
user/password ~selfdestruct~ / ~selfdestruct~):
Create a Postgres role and database that match ~dev.env.example~ (user/password
~selfdestruct~ / ~selfdestruct~):

#+BEGIN_SRC sh
createuser selfdestruct
psql -c "ALTER USER selfdestruct WITH PASSWORD 'selfdestruct';"
createdb -O selfdestruct self-destruct-dev
lein migrate
clj -M:migrate
#+END_SRC

If you use a different role, update ~:database-url~ in ~profiles.clj~ (and
export ~DATABASE_URL~ if you override it in the shell). JDBC connections use
TCP auth; peer auth via Unix sockets is not used.
If you use a different role, update ~DATABASE_URL~ in ~.env~. JDBC connections
use TCP auth; peer auth via Unix sockets is not used.

*** Run the app

After migrations have been applied:

#+BEGIN_SRC sh
lein dev
clj -M:dev-main
#+END_SRC

That starts Garden CSS auto-compile and the Ring server (default port 8000).

Equivalent pieces if you prefer to run them separately:

#+BEGIN_SRC sh
lein garden auto
lein ring server
clj -M:css watch
clj -M:run -- --port 8000
#+END_SRC

~:run~ uses the production entrypoint (~-main~). ~:dev-main~ uses
~-dev-main~ with Ring reload.

*** Running Tests

#+BEGIN_SRC sh
./run-tests-local.sh
#+END_SRC

This recreates ~self-destruct-test~, runs migrations, then ~lein test~.
Ensure the JDBC role in ~profiles.clj~ / ~DATABASE_URL~ can create that database
(or create it yourself and point ~DATABASE_URL~ at it).
This recreates ~self-destruct-test~, runs migrations, then ~clj -M:test~.
Ensure your Postgres role can create that database (or create it yourself and
point ~DATABASE_URL~ at it).


** Continuous Integration

Pull requests and pushes to ~master~ run on GitHub Actions (~.github/workflows/ci.yml~):
Postgres service, ~lein migrate~, then ~lein test~.
Postgres service, ~clj -M:migrate~, ~clj -M:test~, and an uberjar build.


** Database Migrations

- Migratus handles schema changes; files live in ~resources/migrations~.
- Create a migration with ~lein migratus create <descriptive-name>~, then edit the ~up~ / ~down~ SQL.
- Apply migrations with ~lein migrate~ (runs the app ~--migrate~ CLI) or
- Create a new pair of ~*.up.sql~ / ~*.down.sql~ files under ~resources/migrations~
(timestamp prefix, e.g. ~20260731120000-add-thing.up.sql~).
- Apply migrations with ~clj -M:migrate~ or
~java -jar target/self-destruct.jar --migrate~ in production.


** Production Setup
** Production Build

#+BEGIN_SRC sh
clj -T:build uber
#+END_SRC

Writes ~target/self-destruct.jar~ (compiles Garden CSS first, then AOT + uberjar).

Primary target is a FreeBSD home server. See [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]]
for install, ~rc.d~, nginx, certbot, and backup scripts under ~scripts/~.
For FreeBSD production deploy (app user, ~rc.d~, nginx, certbot, backups), see
[[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] and the scripts under ~scripts/~.

Required environment variables (also listed in [[file:.env.example][.env.example]]):

Expand Down
38 changes: 38 additions & 0 deletions build.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
(ns build
(:require [clojure.java.io :as io]
[clojure.tools.build.api :as b]
[garden.core :as garden]))


(def class-dir "target/classes")
(def basis (delay (b/create-basis {:project "deps.edn"})))
(def uber-file "target/self-destruct.jar")
(def css-output "resources/public/css/style.css")


(defn clean [_]
(b/delete {:path "target"})
(b/delete {:path "resources/public/css"}))


(defn compile-css [_]
(require 'self-destruct.css)
(let [style @(resolve 'self-destruct.css/style)]
(io/make-parents css-output)
(spit css-output (apply garden/css {:pretty-print? true} style))
(println "Wrote" css-output)))


(defn uber [_]
(clean nil)
(compile-css nil)
(b/copy-dir {:src-dirs ["src" "resources"]
:target-dir class-dir})
(b/compile-clj {:basis @basis
:ns-compile '[self-destruct.core]
:class-dir class-dir})
(b/uber {:class-dir class-dir
:uber-file uber-file
:basis @basis
:main 'self-destruct.core})
(println "Uberjar written to" uber-file))
Loading
Loading