1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8
9with lib;
10
11let
12 cfg = config.services.coder;
13 name = "coder";
14in
15{
16 options = {
17 services.coder = {
18 enable = mkEnableOption "Coder service";
19
20 user = mkOption {
21 type = types.str;
22 default = "coder";
23 description = ''
24 User under which the coder service runs.
25
26 ::: {.note}
27 If left as the default value this user will automatically be created
28 on system activation, otherwise it needs to be configured manually.
29 :::
30 '';
31 };
32
33 group = mkOption {
34 type = types.str;
35 default = "coder";
36 description = ''
37 Group under which the coder service runs.
38
39 ::: {.note}
40 If left as the default value this group will automatically be created
41 on system activation, otherwise it needs to be configured manually.
42 :::
43 '';
44 };
45
46 package = mkPackageOption pkgs "coder" { };
47
48 homeDir = mkOption {
49 type = types.str;
50 description = ''
51 Home directory for coder user.
52 '';
53 default = "/var/lib/coder";
54 };
55
56 listenAddress = mkOption {
57 type = types.str;
58 description = ''
59 Listen address.
60 '';
61 default = "127.0.0.1:3000";
62 };
63
64 accessUrl = mkOption {
65 type = types.nullOr types.str;
66 description = ''
67 Access URL should be a external IP address or domain with DNS records pointing to Coder.
68 '';
69 default = null;
70 example = "https://coder.example.com";
71 };
72
73 wildcardAccessUrl = mkOption {
74 type = types.nullOr types.str;
75 description = ''
76 If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the root and wildcard domains.
77 '';
78 default = null;
79 example = "*.coder.example.com";
80 };
81
82 environment = {
83 extra = mkOption {
84 type = types.attrs;
85 description = "Extra environment variables to pass run Coder's server with. See Coder documentation.";
86 default = { };
87 example = {
88 CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS = true;
89 CODER_OAUTH2_GITHUB_ALLOWED_ORGS = "your-org";
90 };
91 };
92 file = mkOption {
93 type = types.nullOr types.path;
94 description = "Systemd environment file to add to Coder.";
95 default = null;
96 };
97 };
98
99 database = {
100 createLocally = mkOption {
101 type = types.bool;
102 default = true;
103 description = ''
104 Create the database and database user locally.
105 '';
106 };
107
108 host = mkOption {
109 type = types.str;
110 default = "/run/postgresql";
111 description = ''
112 Hostname hosting the database.
113 '';
114 };
115
116 database = mkOption {
117 type = types.str;
118 default = "coder";
119 description = ''
120 Name of database.
121 '';
122 };
123
124 username = mkOption {
125 type = types.str;
126 default = "coder";
127 description = ''
128 Username for accessing the database.
129 '';
130 };
131
132 password = mkOption {
133 type = types.nullOr types.str;
134 default = null;
135 description = ''
136 Password for accessing the database.
137 '';
138 };
139
140 sslmode = mkOption {
141 type = types.nullOr types.str;
142 default = "disable";
143 description = ''
144 Password for accessing the database.
145 '';
146 };
147 };
148
149 tlsCert = mkOption {
150 type = types.nullOr types.path;
151 description = ''
152 The path to the TLS certificate.
153 '';
154 default = null;
155 };
156
157 tlsKey = mkOption {
158 type = types.nullOr types.path;
159 description = ''
160 The path to the TLS key.
161 '';
162 default = null;
163 };
164 };
165 };
166
167 config = mkIf cfg.enable {
168 assertions = [
169 {
170 assertion =
171 cfg.database.createLocally
172 -> cfg.database.username == name && cfg.database.database == cfg.database.username;
173 message = "services.coder.database.username must be set to ${name} if services.coder.database.createLocally is set true";
174 }
175 ];
176
177 systemd.services.coder = {
178 description = "Coder - Self-hosted developer workspaces on your infra";
179 after = [ "network.target" ];
180 wantedBy = [ "multi-user.target" ];
181
182 environment = cfg.environment.extra // {
183 CODER_ACCESS_URL = cfg.accessUrl;
184 CODER_WILDCARD_ACCESS_URL = cfg.wildcardAccessUrl;
185 CODER_PG_CONNECTION_URL = "user=${cfg.database.username} ${
186 optionalString (cfg.database.password != null) "password=${cfg.database.password}"
187 } database=${cfg.database.database} host=${cfg.database.host} ${
188 optionalString (cfg.database.sslmode != null) "sslmode=${cfg.database.sslmode}"
189 }";
190 CODER_ADDRESS = cfg.listenAddress;
191 CODER_TLS_ENABLE = optionalString (cfg.tlsCert != null) "1";
192 CODER_TLS_CERT_FILE = cfg.tlsCert;
193 CODER_TLS_KEY_FILE = cfg.tlsKey;
194 };
195
196 serviceConfig = {
197 ProtectSystem = "full";
198 PrivateTmp = "yes";
199 PrivateDevices = "yes";
200 SecureBits = "keep-caps";
201 AmbientCapabilities = "CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
202 CacheDirectory = "coder";
203 CapabilityBoundingSet = "CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
204 KillSignal = "SIGINT";
205 KillMode = "mixed";
206 NoNewPrivileges = "yes";
207 Restart = "on-failure";
208 ExecStart = "${cfg.package}/bin/coder server";
209 User = cfg.user;
210 Group = cfg.group;
211 EnvironmentFile = lib.mkIf (cfg.environment.file != null) cfg.environment.file;
212 };
213 };
214
215 services.postgresql = lib.mkIf cfg.database.createLocally {
216 enable = true;
217 ensureDatabases = [
218 cfg.database.database
219 ];
220 ensureUsers = [
221 {
222 name = cfg.user;
223 ensureDBOwnership = true;
224 }
225 ];
226 };
227
228 users.groups = optionalAttrs (cfg.group == name) {
229 "${cfg.group}" = { };
230 };
231 users.users = optionalAttrs (cfg.user == name) {
232 ${name} = {
233 description = "Coder service user";
234 group = cfg.group;
235 home = cfg.homeDir;
236 createHome = true;
237 isSystemUser = true;
238 };
239 };
240 };
241 meta.maintainers = pkgs.coder.meta.maintainers;
242}