1{
2 config,
3 lib,
4 pkgs,
5 options,
6 ...
7}:
8let
9
10 cfg = config.services.jupyter;
11
12 package = pkgs.python3.withPackages (
13 ps:
14 [
15 cfg.package
16 ]
17 ++ cfg.extraPackages
18 );
19
20 kernels = (
21 pkgs.jupyter-kernel.create {
22 definitions = if cfg.kernels != null then cfg.kernels else pkgs.jupyter-kernel.default;
23 }
24 );
25
26 notebookConfig = pkgs.writeText "jupyter_server_config.py" ''
27 ${cfg.notebookConfig}
28 c.ServerApp.password = "${cfg.password}"
29 '';
30
31in
32{
33 meta.maintainers = with lib.maintainers; [
34 aborsu
35 b-m-f
36 ];
37
38 options.services.jupyter = {
39 enable = lib.mkEnableOption "Jupyter development server";
40
41 ip = lib.mkOption {
42 type = lib.types.str;
43 default = "localhost";
44 description = ''
45 IP address Jupyter will be listening on.
46 '';
47 };
48
49 package = lib.mkPackageOption pkgs [
50 "python3"
51 "pkgs"
52 "jupyter"
53 ] { };
54
55 extraPackages = lib.mkOption {
56 type = lib.types.listOf lib.types.package;
57 default = [ ];
58 example = lib.literalExpression ''
59 [
60 pkgs.python3.pkgs.nbconvert
61 pkgs.python3.pkgs.playwright
62 ]
63 '';
64 description = "Extra packages to be available in the jupyter runtime environment";
65 };
66 extraEnvironmentVariables = lib.mkOption {
67 description = "Extra environment variables to be set in the runtime context of jupyter notebook";
68 default = { };
69 example = lib.literalExpression ''
70 {
71 PLAYWRIGHT_BROWSERS_PATH = "''${pkgs.playwright-driver.browsers}";
72 PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = "true";
73 }
74 '';
75 inherit (options.environment.variables) type apply;
76 };
77
78 command = lib.mkOption {
79 type = lib.types.str;
80 default = "jupyter notebook";
81 example = "jupyter lab";
82 description = ''
83 Which command the service runs. Note that not all jupyter packages
84 have all commands, e.g. `jupyter lab` isn't present in the `notebook` package.
85 '';
86 };
87
88 port = lib.mkOption {
89 type = lib.types.port;
90 default = 8888;
91 description = ''
92 Port number Jupyter will be listening on.
93 '';
94 };
95
96 notebookDir = lib.mkOption {
97 type = lib.types.str;
98 default = "~/";
99 description = ''
100 Root directory for notebooks.
101 '';
102 };
103
104 user = lib.mkOption {
105 type = lib.types.str;
106 default = "jupyter";
107 description = ''
108 Name of the user used to run the jupyter service.
109 For security reason, jupyter should really not be run as root.
110 If not set (jupyter), the service will create a jupyter user with appropriate settings.
111 '';
112 example = "aborsu";
113 };
114
115 group = lib.mkOption {
116 type = lib.types.str;
117 default = "jupyter";
118 description = ''
119 Name of the group used to run the jupyter service.
120 Use this if you want to create a group of users that are able to view the notebook directory's content.
121 '';
122 example = "users";
123 };
124
125 password = lib.mkOption {
126 type = lib.types.str;
127 description = ''
128 Password to use with notebook.
129 Can be generated following: https://jupyter-server.readthedocs.io/en/stable/operators/public-server.html#preparing-a-hashed-password
130 '';
131 example = "argon2:$argon2id$v=19$m=10240,t=10,p=8$48hF+vTUuy1LB83/GzNhUg$J1nx4jPWD7PwOJHs5OtDW8pjYK2s0c1R3rYGbSIKB54";
132 };
133
134 notebookConfig = lib.mkOption {
135 type = lib.types.lines;
136 default = "";
137 description = ''
138 Raw jupyter config.
139 Please use the password configuration option to set a password instead of passing it in here.
140 '';
141 };
142
143 kernels = lib.mkOption {
144 type = lib.types.nullOr (
145 lib.types.attrsOf (
146 lib.types.submodule (
147 import ./kernel-options.nix {
148 inherit lib pkgs;
149 }
150 )
151 )
152 );
153
154 default = null;
155 example = lib.literalExpression ''
156 {
157 python3 = let
158 env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
159 ipykernel
160 pandas
161 scikit-learn
162 ]));
163 in {
164 displayName = "Python 3 for machine learning";
165 argv = [
166 "''${env.interpreter}"
167 "-m"
168 "ipykernel_launcher"
169 "-f"
170 "{connection_file}"
171 ];
172 language = "python";
173 logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
174 logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
175 extraPaths = {
176 "cool.txt" = pkgs.writeText "cool" "cool content";
177 };
178 };
179 }
180 '';
181 description = ''
182 Declarative kernel config.
183
184 Kernels can be declared in any language that supports and has the required
185 dependencies to communicate with a jupyter server.
186 In python's case, it means that ipykernel package must always be included in
187 the list of packages of the targeted environment.
188 '';
189 };
190 };
191
192 config = lib.mkMerge [
193 (lib.mkIf cfg.enable {
194 systemd.services.jupyter = {
195 description = "Jupyter development server";
196
197 after = [ "network.target" ];
198 wantedBy = [ "multi-user.target" ];
199
200 # TODO: Patch notebook so we can explicitly pass in a shell
201 path = [ pkgs.bash ]; # needed for sh in cell magic to work
202
203 environment = {
204 JUPYTER_PATH = toString kernels;
205 } // cfg.extraEnvironmentVariables;
206
207 serviceConfig = {
208 Restart = "always";
209 ExecStart = ''
210 ${package}/bin/${cfg.command} \
211 --no-browser \
212 --ip=${cfg.ip} \
213 --port=${toString cfg.port} --port-retries 0 \
214 --notebook-dir=${cfg.notebookDir} \
215 --JupyterApp.config_file=${notebookConfig}
216
217 '';
218 User = cfg.user;
219 Group = cfg.group;
220 WorkingDirectory = "~";
221 };
222 };
223 })
224 (lib.mkIf (cfg.enable && (cfg.group == "jupyter")) {
225 users.groups.jupyter = { };
226 })
227 (lib.mkIf (cfg.enable && (cfg.user == "jupyter")) {
228 users.extraUsers.jupyter = {
229 inherit (cfg) group;
230 home = "/var/lib/jupyter";
231 createHome = true;
232 isSystemUser = true;
233 useDefaultShell = true; # needed so that the user can start a terminal.
234 };
235 })
236 ];
237}